package com.globallogic.javatrainings.sergiiivanov.model;

import java.io.Serializable;

import com.globallogic.javatrainings.sergiiivanov.resources.ErrorCodes;

/**
 * Contains information about course (learning program)<br>
 * Linked to {@link University} via {@link CoursesManager} as
 * {@link University#courses}<br>
 * Typical call to courses collection is <b>university.courses...</b><br>
 * 
 * @author Sergey
 * 
 */
public class Course implements Serializable, MyArrayToNamesCovertable {
    private static final long serialVersionUID = 1L;
    private Const.Discipline discipline;
    private MyArray<Student> enrolledStudents;
    private MyArray<Lecture> lectures;
    private Teacher teacher;
    private String name;
    private CoursesManager manager;
    private MyArray<ScoreRecord> scores;

    /**
     * Quick way to create {@link Course} in tests
     * 
     * @param courceName
     *            - name, calls {@link #setName(String)}
     * @param newCourseDiscipline
     *            - {@link Const.Discipline} of the course, calls
     *            {@link #setDiscipline(Const.Discipline)}
     * @throws UniversityWarning
     *             when name is not valid (null or empty) nor unique
     */
    public Course(String courceName, Const.Discipline newCourseDiscipline) throws UniversityWarning {
        initialize();
        setName(courceName);
        setDiscipline(newCourseDiscipline);
    }

    /**
     * Creates new empty {@link Course}.<br>
     * Used by {@link CoursesManager#addCource(String, Const.Discipline)}
     */
    public Course() {
        initialize();
    }

    /**
     * Adds new {@link Lecture} to the course
     * 
     * @param lectureName
     *            - unique not null not empty name of the lecture (lesson)
     * @param lectureType
     *            - LectureType of the lecture {Lecture, Practice, Examination}
     * @param lectureDescription
     *            - free form description (set as is)
     * @throws UniversityWarning
     *             when name is not valid, see {@link #haveDuplicates(String)}
     */
    public void addLecture(
            String lectureName,
            Lecture.LectureType lectureType,
            String lectureDescription) throws UniversityWarning {

        Lecture newLecture = new Lecture();
        newLecture.setCourse(this);
        newLecture.setName(lectureName);
        newLecture.setType(lectureType);
        newLecture.setDescription(lectureDescription);
        lectures.add(newLecture);
    }

    /**
     * Called from {@link ScoreRecord}<br>
     * Saves link to the {@link ScoreRecord} for {@link ScoresProcessor} reports
     * 
     * @param scoreRecord
     *            link to {@link ScoreRecord}
     */
    void addScoreLink(ScoreRecord scoreRecord) {
        if (scores.indexOf(scoreRecord) < 0) {
            scores.add(scoreRecord);
        }
    }

    /**
     * Calculates available places in the group as<br>
     * Used by {@link #enrollStudent(Student)}<br>
     * {@link #availablePlaces()} = MIN( {@link Room#getCapacity()} ) -
     * {@link #countStudents()}<br>
     * <blockquote> Linked {@link Lecture lectures} are
     * {@link Lecture#setRoom(Room) appointed} to {@link Room rooms}
     * </blockquote>
     * 
     * @return number of available places
     */
    public int availablePlaces() {
        int result = Integer.MAX_VALUE;
        for (Lecture l : lectures) {
            result = Math.min(result, l.availablePlaces());
        }
        return result;
    }

    /**
     * Counts lectures {@link #addLecture(String, Lecture.LectureType, String)
     * added} to the course
     * 
     * @return number of lectures
     */

    /**
     * Counts students {@link #enrollStudent(Student) enrolled} to the course<br>
     * Used by <li>{@link Lecture#availablePlaces()} <li>
     * {@link Lecture#checkRoomSize(Room)} <li>{@link #availablePlaces()}
     * 
     * @return number of students
     */
    public int countStudents() {
        return enrolledStudents.size();
    }

    /**
     * Removes lecture from the course and cleanup links by calling
     * {@link Lecture#removeLinks()}
     * 
     * @param lecture
     *            -{@link Lecture} to remove
     * @throws UniversityWarning
     *             when lecture not found in course
     * @throws UniversityFailure
     *             when application crashes
     */
    public void removeLecture(Lecture lecture) throws UniversityWarning {
        if (lecture == null || lectures.indexOf(lecture) < 0) {
            throw new UniversityWarning(ErrorCodes.LECTURE_NOT_FOUND);
        }
        lectures.remove(lecture);
        lecture.removeLinks();
    }

    /**
     * Removes {@link Student} from the course and clears links between student
     * and course.
     * 
     * @param student
     *            - {@link Student}
     * @throws UniversityWarning
     *             when {@link Student} is not enrolled
     * @throws UniversityFailure
     *             when application crashes
     */
    public void disenrollStudent(Student student) throws UniversityWarning, UniversityFailure {
        int index = enrolledStudents.indexOf(student);
        if (index >= 0) {
            enrolledStudents.remove(index);
            student.removeCourse(this);
        } else {
            throw new UniversityWarning(ErrorCodes.STUDENT_NOT_ENROLLED_TO + getName());
        }
    }

    /**
     * Adds {@link Student} to the course and generates links between student
     * and course.
     * 
     * @param student
     *            - {@link Student}
     * @throws UniversityWarning
     *             when {@link Student} is already enrolled or <b>null</b>
     */
    public void enrollStudent(Student student) throws UniversityWarning {
        if (student == null) {
            throw new UniversityWarning(ErrorCodes.STUDENT_NOT_FOUND);
        }
        if (enrolledStudents.indexOf(student) < 0) {
            if (availablePlaces() < 1) {
                throw new UniversityWarning(ErrorCodes.COURSE_NO_PLACES);
            }
            enrolledStudents.add(student);
            student.addCourse(this);
        } else {
            throw new UniversityWarning(ErrorCodes.STUDENT_ALREADY_ENROLLED+student.getName());
        }
    }

    /**
     * Returns {@link Const.Discipline} as it was set by
     * {@link #setDiscipline(Const.Discipline)}
     * 
     * @return {@link Const.Discipline} or <b>null</b>
     */
    public Const.Discipline getDiscipline() {
        return discipline;
    }

    /**
     * Calculates null-safe {@link Const.Discipline#getRepresentation()
     * representation} of the discipline.<br>
     * Used in GUI for printing out the discipline name without extra checks
     * 
     * @return result of {@link Const.Discipline#getRepresentation()} or
     *         {@link Const#STRING_EMPTY} if discipline is <b>null</b>
     */
    public String getDisciplinePrint() {
        return getDiscipline() == null ? Const.STRING_EMPTY : getDiscipline().getRepresentation();
    }

    /**
     * Gets {@link MyArray#copy()} of {@link #enrollStudent(Student) enrolled}
     * {@link Student}s
     * 
     * @return {@link MyArray} of {@link Student}
     */
    public MyArray<Student> getEnrolledStudents() {
        return enrolledStudents.copy();
    }

    /**
     * Gets {@link Lecture} stored under <b>index</b><br>
     * Used in GUI for quick access to selected lecture
     * 
     * @param index
     *            valid index
     * @return {@link Lecture}
     * @throws UniversityWarning
     *             when index is out of collection
     * @throws UniversityFailure
     *             when application crashes
     */
    public Lecture getLectureByIndex(int index) throws UniversityWarning, UniversityFailure {
        if (index < 0 || lectures.size() <= index) {
            throw new UniversityWarning(ErrorCodes.LECTURE_NOT_FOUND);
        }
        return lectures.get(index);
    }

    /**
     * Returns {@link MyArray#copy()} of
     * {@link #addLecture(String, Lecture.LectureType, String) added}
     * {@link Lecture}s
     * 
     * @return {@link MyArray} of {@link Lecture}
     */
    public MyArray<Lecture> getLectures() {
        return lectures.copy();
    }

    /**
     * Returns name of course.<br>
     * Part of {@link MyArrayToNamesCovertable} mechanics
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * Returns two dimensional, print-ready array of scores, linked to the
     * course<br>
     * Uses {@link ScoresProcessor} for getting table from {@link ScoreRecord}
     * 
     * @return String[][] array; see {@link ScoresProcessor} for details
     */
    public String[][] getScoreTable() {
        ScoresProcessor sp = new ScoresProcessor();
        sp.setScoresCollection(scores);
        return sp.getTable();
    }

    /**
     * returns {@link Teacher} as it was set by {@link #setTeacher(Teacher)}
     * 
     * @return {@link Teacher} of <b>null</b>
     */
    public Teacher getTeacher() {
        return this.teacher;
    }

    /**
     * Returns null-safe {@link Teacher#getName()} of the {@link Teacher} set by
     * {@link #setTeacher(Teacher)}
     * 
     * @return {@link Teacher#getName()} or {@link Const#STRING_EMPTY} if
     *         teacher id <b>null</b>
     */
    public String getTeacherPrint() {
        return getTeacher() == null ? Const.STRING_EMPTY : getTeacher().getName();

    }

    /**
     * Throws {@link UniversityWarning} if found {@link Lecture} with
     * {@link Lecture#getName()} equal to provided <b>newName</b><br>
     * Used by <li>{@link Lecture#setName(String)} - also called from
     * {@link #addLecture(String, Lecture.LectureType, String)} <li>
     * {@link Lecture#setCourse(Course)}<br>
     * Do not use it for lectures added into course - result will be always
     * positive as it will found itself
     * 
     * @param newName
     *            - name to check
     * @throws UniversityWarning
     *             when lecture with the same name found
     */
    public void haveDuplicates(String newName) throws UniversityWarning {
        for (Lecture l : lectures) {
            if (l.getName().equals(newName)) {
                throw new UniversityWarning(ErrorCodes.LECTURE_NOT_UNIQUE + newName);
            }
        }
    }

    /**
     * Checks if {@link Student} is {@link #enrollStudent(Student) enrolled} to
     * course or not.
     * 
     * @param student
     *            instance of {@link Student} to check
     * @return <li>true - student enrolled <li>false - student not enrolled
     */
    public boolean isStudentEnrolled(Student student) {
        if (enrolledStudents.indexOf(student) > -1) {
            return true;
        }
        return false;
    }

    /**
     * Sets {@link Const.Discipline} for the course if there are no
     * {@link ScoreRecord} are {@link #addScoreLink(ScoreRecord) linked} to the
     * course
     * 
     * @param newCourseDiscipline
     *            - new {@link Const.Discipline}
     * @throws UniversityWarning
     *             when discipline could not be changed due to existing links to
     *             {@link ScoreRecord}s
     */
    public void setDiscipline(Const.Discipline newCourseDiscipline) throws UniversityWarning {
        if (newCourseDiscipline != null && !newCourseDiscipline.equals(discipline)) {
            if (scores.size() > 0) {
                throw new UniversityWarning(ErrorCodes.COURSE_HAVE_SCORES_CANT_CHANGE_DISCIPLINE);
            }
            discipline = newCourseDiscipline;
        }

    }

    /**
     * Updated name if {@link #validateName(String) valid} newCourseName
     * provided (not null, not empty, unique)
     * 
     * @param newCourseName
     *            - valid new name
     * @throws UniversityWarning
     *             when validation failed
     */
    public void setName(String newCourseName) throws UniversityWarning {
        if (newCourseName != null && !Const.STRING_EMPTY.equals(newCourseName)){ 
            if(!newCourseName.equals(name)) {
                validateName(newCourseName);
                name = newCourseName;
            }
        }else{
            throw new UniversityWarning(ErrorCodes.COURSE_NAME_NULL);
        }
    }

    /**
     * Sets new {@link Teacher} to the course and updates links between teacher
     * and course
     * 
     * @param newTeacher
     *            - new {@link Teacher}
     * @see Teacher#removeCourse(Course)
     * @see Teacher#addCourse(Course)
     */
    public void setTeacher(Teacher newTeacher) {
        if (teacher != null) {
            teacher.removeCourse(this);
        }
        this.teacher = newTeacher;
        if (newTeacher != null && newTeacher.getCourses().indexOf(this) < 0){
            newTeacher.addCourse(this);
        }
    }

    private void initialize() {
        setTeacher(null);
        enrolledStudents = new MyArray<Student>(new Student[0]);
        lectures = new MyArray<Lecture>(new Lecture[0]);
        scores = new MyArray<ScoreRecord>();
    }

    private void validateName(String courceTitle) throws UniversityWarning {
        if (courceTitle == null || Const.STRING_EMPTY.equals(courceTitle)) {
            throw new UniversityWarning(ErrorCodes.COURSE_NAME_NULL);
        }
        if (manager != null)
            manager.hasDuplicates(courceTitle);
    }

    /**
     * Sets {@link CoursesManager} as a manager for this course.<br>
     * Once set, it may not be changed.<br>
     * Manager links courses to university and defines the scope of unique names<br>
     * Called from {@link CoursesManager#addCource(String, Const.Discipline)}
     * 
     * @param newManager
     *            - {@link CoursesManager}
     * @throws UniversityWarning
     */
    void setManager(CoursesManager newManager) throws UniversityWarning {
        if (newManager != null && manager == null) {
            manager = newManager;
        }
        else {
            throw new UniversityWarning(ErrorCodes.COURSE_NOT_REASSIGNED);
        }
    }

    /**
     * Removes all links from the course and
     * {@link #addLecture(String, Lecture.LectureType, String) added} lectures.<br>
     * Called from {@link CoursesManager#removeCourse(Course)}<br>
     * <b>Operations in batch:</b> <li> {@link #disenrollStudent(Student)
     * disenroll all students} <li> {@link #removeLecture(Lecture) remove all
     * lectures} <li> {@link ScoreRecord#removeCourse() remove links to score}
     * <li> {@link #setTeacher(Teacher) setTeacher(null)}
     * 
     * @throws UniversityFailure
     *             when {@link #disenrollStudent(Student)} or
     *             {@link #removeLecture(Lecture)} caused application crash
     */
    void removeLinks() throws UniversityFailure {
        for (Student s : enrolledStudents.copy()) {
            try {
                disenrollStudent(s);
            } catch (UniversityWarning e) {}
        }
        for (Lecture l : lectures.copy()) {
            try {
                removeLecture(l);
            } catch (UniversityWarning e) {}
        }
        for (ScoreRecord sr : scores.copy()) {
            sr.removeCourse();
        }
        scores.clear();

        setTeacher(null);

    }

    /**
     * Removes link to score. used when student removed with all score records.
     * 
     * @param scoreRecord
     *            {@link ScoreRecord} to unlink
     */
    void removeScore(ScoreRecord scoreRecord) {
        scores.remove(scoreRecord);
    }

    public MyArray<Room> getAppointedRooms() {
        MyArray<Room> result = new MyArray<Room>();
        for(Lecture l: lectures){
            if(l != null && l.getRoom() != null && result.indexOf(l.getRoom())<0)
                result.add(l.getRoom());
        }
        return result;
    }

    void clearTeacher() {
        teacher = null;
    }

}
